package fabricelaborde.jinsc.jinscapi;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;

/**
 *
 * @author flaborde le 17 septembre 2010
 *
 * Copyright Fabrice LABORDE [fabrice dot laborde at gmail.com]
 */
public class JINSCApi {

    private static JINSCApi instance = null;

    /**
     * Private constructor - singleton use
     */
    private JINSCApi(){
    }

    public static JINSCApi getInstance(){
        if (instance==null){
            instance = new JINSCApi();
        }
        return instance ;
    }

    public void validateINSC(ValueKey insc) throws JINSCException{
        if (insc.getValue()==null){
            throw new JINSCException("insc value cannot be null", new IllegalArgumentException("insc"));
        }
        if (insc.getKey()==null){
            throw new JINSCException("insc key cannot be null", new IllegalArgumentException("insc"));
        }

        if (!computeNirKey(insc.getValue()).equals(insc.getKey())){
            throw new JINSCException("insc key is not verified");
        }
    }


    private void validateNir(ValueKey nir) throws JINSCException{
        if (nir.getValue()==null){
            throw new JINSCException("nir value cannot be null", new IllegalArgumentException("nir"));
        }
        if (nir.getKey()==null){
            throw new JINSCException("nir key cannot be null", new IllegalArgumentException("nir"));
        }

        if (nir.getValue().startsWith("8")){
            throw new JINSCException("nir ["+nir.getValue()+"]cannot be a temporary nir");
        }

        if (!computeNirKey(nir.getValue()).equals(nir.getKey())){
            throw new JINSCException("nir key is not verified");
        }
    }

    /**
     * according to http://xml.insee.fr/schema/nir.html ref 4.1 on CORSE Dept exception Management
     *  "La clé de contrôle du NIR est un nombre à deux chiffres dont la valeur est le complément à 97 du reste de la division du nombre formé par le NIR par 97. Cette formule a été adaptée aux numéros de départements alphabétiques (2A et 2B) de la manière suivante :
     *  Les caractères alphabétiques sont remplacés par des "0".
     *  On soustrait du nombre à 13 chiffres ainsi obtenu la valeur 1000000 si le caractère alphabétique remplacé par un zéro était la lettre A, ou la valeur 2000000 si c'était la lettre B."
     *
     * but it dosen't work against checks on ASIP
     * method trying substitution 2A -> 19 and 2B -> 18 seems to be better way... http://www.foxprofr.com/codes/CALCUL-CLE-MATRICULE-SECURITE-SOCIALE_27247.aspx
     **/
    private String computeNirKey(String nirValue){
        String preparedNirValue = nirValue.replace("2A", "19");
        preparedNirValue = preparedNirValue.replace("2B", "18");

        /*
        if (nirValue.indexOf("2A")!=-1){
            // if it was a 'A'
            preparedNirValue = preparedNirValue.min(new BigInteger("1000000"));
        }
        else if(nirValue.indexOf("2B") != -1){
            // if it was a 'B'
            preparedNirValue = preparedNirValue.min(new BigInteger("2000000"));
        }
         * 
         */

        return String.format("%02d", 97 - new BigInteger(preparedNirValue).mod(new BigInteger("97")).longValue());
    }

    public String prepareFirstName(String firstName) {
        // special chararacters
        String preparedFirstName = firstName.replaceAll("[ÀÁÂÃÄÅÆàáâãæäå]", "A");
        preparedFirstName = preparedFirstName.replaceAll("[ÈÉÊËèéêë]", "E");
        preparedFirstName = preparedFirstName.replaceAll("[Ðð]", "D");
        preparedFirstName = preparedFirstName.replaceAll("[ÒÓÔÕÖØòóôõöø]", "O");
        preparedFirstName = preparedFirstName.replaceAll("[ŸÝýÿ]", "Y");
        preparedFirstName = preparedFirstName.replaceAll("[Çç]", "C");
        preparedFirstName = preparedFirstName.replaceAll("[ÌÍÎÏìíîï]", "I");
        preparedFirstName = preparedFirstName.replaceAll("[Ññ]", "N");
        preparedFirstName = preparedFirstName.replaceAll("[ÙÚÛÜùúûü]", "U");
        preparedFirstName = preparedFirstName.replaceAll("[ß]", "B");
        preparedFirstName = preparedFirstName.replaceAll("[Œ]", "OE");
        preparedFirstName = preparedFirstName.replaceAll("[Šš]", "S");
        preparedFirstName = preparedFirstName.replaceAll("[žŽ]", "Z");
        // every word no alaphanumeric
        preparedFirstName = preparedFirstName.replaceAll("[^a-zA-Z0-9 ]"," ");
        // remove spaces 
        preparedFirstName = preparedFirstName.replace(" ", "");
        // upper case
        preparedFirstName = preparedFirstName.toUpperCase();

        // length operations
        if (preparedFirstName.length() < 10) {
            StringBuilder builder = new StringBuilder("          "); // ten spaces char
            builder.replace(0, preparedFirstName.length(), preparedFirstName);
            preparedFirstName = builder.toString();
        } else {
            preparedFirstName = preparedFirstName.substring(0, 10);
        }
        return preparedFirstName;
    }

    public ValueKey getINSC(String firstName, String birthDate, ValueKey nir) throws JINSCException{
        if (firstName==null){
            throw new JINSCException("firstName cannot be null");
        }
        if (birthDate==null){
            throw new JINSCException("birthDate cannot be null");
        }
        if (birthDate.length()!=6){
            throw new JINSCException("birthDate has to be a string in yyMMdd date format");
        }
        if (nir==null){
            throw new JINSCException("birthDate cannot be null");
        }

        // 2.4.2 : validate nir
        validateNir(nir);

        // 2.4.4 : compute algo
            // 2.4.4-1 - normalize (against 2.4.3 rules)
            String preparedFirstName = prepareFirstName(firstName);
            // 2.4.4-3 - create seed
            String seed = preparedFirstName + birthDate + nir.getValue();
            if (seed.length()!=29){
                throw new JINSCException("seed has to be a string on 29 characters lenght");
            }
            // 2.4.4-4 - get hash for seed
            String hashedSeed = hash(seed);
            // 2.4.4-5 - convert hash to a numeric value : get ins-c value
            String inscValue = covertHashToNumeric(hashedSeed);
            // 2.4.4-6 - ins-c key computing : same computing as nir key
            String inscKey = computeNirKey(inscValue);

        return new ValueKey(inscValue, inscKey);
    }

    private String hash(String seed) throws JINSCException {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] hash = md.digest(seed.getBytes());

            StringBuilder hashedSeed = new StringBuilder();
            for (byte b : hash) {
                hashedSeed.append(String.format("%02x", b));
            }
            return hashedSeed.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new JINSCException("Error while generating hash for seed", e);
        }
    }

    private String covertHashToNumeric(String hashedSeed){
        String hexValue = hashedSeed.substring(0, 16);
        DecimalFormat df = new DecimalFormat("00000000000000000000");
        BigInteger numericValue = new BigInteger(hexValue, 16);
        String completeStringValue = numericValue.toString();
        if (completeStringValue.length()>=20){
            return completeStringValue.substring(0,20);
        }
        return df.format(numericValue);
    }
}
